package anet

import (
	"fmt"
	"sync"
	"sync/atomic"

	"gitee.com/AlexWoo/arpc/alog"
	"google.golang.org/protobuf/proto"
)

var (
	anetLctx = &alog.LogCtx{
		Prefix:   func() string { return "Net" },
		Suffix:   func() string { return "" },
		LogLevel: alog.LOGINFO,
	}
)

type NetResouce struct {
	// Connection
	ConnectionNalloc int32
	ConnectionNfree  int32

	// Session
	SessionNalloc int32
	SessionNfree  int32

	// Ringbuf
	RingbufNalloc int32
	RingbufNfree  int32

	// Timer
	TimerNalloc int32
	TimerNfree  int32

	// Event
	EventNalloc int32
	EventNfree  int32
}

func GetanetResource() *NetResouce {
	br := &NetResouce{}

	br.ConnectionNalloc, br.ConnectionNfree = cPool.Resource()
	br.SessionNalloc, br.SessionNfree = sPool.Resource()
	br.RingbufNalloc, br.RingbufNfree = rbPool.Resource()
	br.TimerNalloc, br.TimerNfree = rtm.tPool.Resource()
	br.EventNalloc, br.EventNfree = ePool.Resource()

	return br
}

type OnConnect func(Conn)
type OnDisconnect func(Conn, error)

type Handler interface {
	OnMsg(c Conn, bts []byte)
}

type Connect func(string, *Conf, Handler, *sync.WaitGroup, OnConnect, OnDisconnect) Conn

type Dispatcher interface {
	Dispatch(m *Msg)
}

type Conn interface {
	Send(bts []byte) error
	Close()
	Lctx() *alog.LogCtx
	Prefix() string
	Suffix() string
}

type Net struct {
	conf *Conf
	wg   *sync.WaitGroup
	disp Dispatcher

	serialId uint64

	lctx    *alog.LogCtx
	connect Connect

	recvQ  chan *message
	sendQ  chan *message
	eventQ chan *event
	stopC  chan bool

	conns    map[string]*connection // only for client session
	sessions map[string]*session
}

func CreateNet(conf *Conf, connect Connect, disp Dispatcher, wg *sync.WaitGroup) *Net {
	bn := &Net{
		conf:     conf,
		wg:       wg,
		disp:     disp,
		serialId: 0,
		lctx:     anetLctx,
		connect:  connect,
		recvQ:    make(chan *message, conf.QueueSize),
		sendQ:    make(chan *message, conf.QueueSize),
		eventQ:   make(chan *event, conf.QueueSize),
		stopC:    make(chan bool, 1),
		conns:    map[string]*connection{},
		sessions: map[string]*session{},
	}

	alog.NewWsLog(&conf.Log)
	createTimerManager()

	bn.lctx.LogLevel = alog.Level[conf.Log.LogLevel]

	wg.Add(1)
	go bn.loop(wg)

	return bn
}

// Make sure all msg send to application is legal
func (bn *Net) checkMsg(m *Msg) (uint32, error) {
	if m.AppId == "" {
		return UnexpectedAppID, fmt.Errorf("unexpectedAppID %s", m.Str())
	}

	if m.ChannelId == "" {
		return UnexpectedChannelID, fmt.Errorf("unexpectedChannelID %s", m.Str())
	}

	if m.UserId == "" {
		return UnexpectedUserID, fmt.Errorf("unexpectedUserID %s", m.Str())
	}

	return 0, nil
}

func (bn *Net) OnMsg(c Conn, bts []byte) {
	m := &Msg{}
	if err := proto.Unmarshal(bts, m); err != nil {
		alog.LogError(bn.lctx, "Unmarshal message error %v", err)
		return
	}

	recvm := getMessage("", c, m)

	alog.LogInfo(bn.lctx, "OnMsg: %p[%s]", m, m.Str())

	bn.recvQ <- recvm
}

func (bn *Net) Send(m *Msg, addr string) {
	if m.Code == 0 {
		m.SerialId = atomic.AddUint64(&bn.serialId, 1)
	}

	sendm := getMessage(addr, nil, m)

	alog.LogInfo(bn.lctx, "Send: %p[%s]", m, m.Str())

	bn.sendQ <- sendm
}

func (bn *Net) Stop() {
	bn.stopC <- true
}

func sessName(appid string, channelid string, userid string) string {
	return appid + "/" + channelid + "/" + userid
}

func (bn *Net) event(ev *event) {
	bn.eventQ <- ev
}

func (bn *Net) postHandle(sess *session) {
	alog.LogDebug(sess.lctx, "posthandler")

	if sess.state == sessionClosed {
		if sess.isClient() {
			c := bn.conns[sess.serverAddr()]
			c.unbindSession(sess)
			if len(c.sess) == 0 {
				delete(bn.conns, sess.serverAddr())
				alog.LogInfo(sess.lctx, "posthandler delete session connect [%s]", sess.serverAddr())
			}
		}

		alog.LogInfo(sess.lctx, "posthandler delete session [%s]", sess.name)
		delete(bn.sessions, sess.name)
		sess.close()
	}
}

func (bn *Net) recvHandle(msg *message) {
	if code, err := bn.checkMsg(msg.m); code != 0 {
		alog.LogError(bn.lctx, "CheckMsg error when receive msg[%s], err: %s", msg.m.Str(), err.Error())
		if msg.m.Code == 0 {
			bts, _ := proto.Marshal(Response(msg.m, code, nil))
			msg.c.Send(bts)
		}

		putMessage(msg)

		return
	}
	name := sessName(msg.m.AppId, msg.m.ChannelId, msg.m.UserId)

	sess := bn.sessions[name]
	if sess == nil {
		if !(msg.m.Type == MsgType_Join && msg.m.Code == 0) {
			alog.LogError(bn.lctx, "Recv msg[%s], but session[%s] is not exist", msg.m.Str(), name)
			if msg.m.Code == 0 {
				res := Response(msg.m, SessionNotExist, nil)
				bts, _ := proto.Marshal(res)
				msg.c.Send(bts)
			}

			putMessage(msg)

			return
		}

		// Join Request
		sess = createSession(name, msg.m.AppId, msg.m.ChannelId, msg.m.UserId, msg.m.TraceId, bn, msg.c, msg.addr)
		bn.sessions[name] = sess
		alog.LogInfo(bn.lctx, "recvHandle Create Session[%s]", name)
	}

	sess.recvMsg(msg)

	alog.LogDebug(bn.lctx, "recvHandle session [%s] state [%d]", name, sess.state)
	bn.postHandle(sess)
}

func (bn *Net) sendHandle(msg *message) {
	if code, err := bn.checkMsg(msg.m); code != 0 {
		alog.LogError(bn.lctx, "CheckMsg error when send msgp[%s], err: %s", msg.m.Str(), err.Error())
		putMessage(msg)

		return
	}

	name := sessName(msg.m.AppId, msg.m.ChannelId, msg.m.UserId)

	sess := bn.sessions[name]
	if sess == nil {
		if !(msg.m.Type == MsgType_Join && msg.m.Code == 0 && msg.addr != "") {
			alog.LogError(bn.lctx, "Send msg[%s], but session[%s] is not exist", msg.m.Str(), name)

			putMessage(msg)

			return
		}

		// find connection
		c := bn.conns[msg.addr]
		if c == nil {
			conn := bn.connect(msg.addr, bn.conf, bn, bn.wg, func(c Conn) {
				bn.event(getEvent(msg.addr, eventType_connected, 0))
			}, nil)
			c = getConnection(conn)
			bn.conns[msg.addr] = c
		}

		// Join Request
		sess = createSession(name, msg.m.AppId, msg.m.ChannelId, msg.m.UserId, msg.m.TraceId, bn, c.conn, msg.addr)
		bn.sessions[name] = sess
		c.bindSession(sess)
		alog.LogInfo(bn.lctx, "sendHandle Create Session[%s]", name)
	}

	sess.sendMsg(msg)
	alog.LogDebug(bn.lctx, "sendHandle session [%s] state [%d]", name, sess.state)
	bn.postHandle(sess)
}

func (bn *Net) eventHandle(ev *event) {
	defer func() {
		putEvent(ev)
	}()

	alog.LogDebug(bn.lctx, "Start eventHandle eventName [%s] Event [%v]", ev.name, ev.typ)

	if ev.typ == eventType_connected {
		c := bn.conns[ev.name]
		if c == nil {
			alog.LogError(bn.lctx, "Cannot find connection when connected: %s", ev.String())
			return
		}

		alog.LogInfo(c.conn.Lctx(), "Net connected")

		for _, sess := range c.sess {
			sess.state = sessionNormal
			sess.updateConn(c.conn)
			sess.connect()

			alog.LogDebug(bn.lctx, "eventHandle session [%s] state [%d]", sess.name, sess.state)
			bn.postHandle(sess)
		}
	} else {
		sess := bn.sessions[ev.name]
		if sess == nil {
			alog.LogError(bn.lctx, "Cannot find session when timeout: %s", ev.String())
			return
		}

		switch ev.typ {
		case eventType_sessTimeout:
			sess.sessTimeout()
		case eventType_recvTimeout:
			sess.recvTimeout(ev)
		case eventType_sendTimeout:
			sess.sendTimeout(ev)
		}

		alog.LogDebug(bn.lctx, "eventHandle session [%s] state [%d], Event [%v]", sess.name, sess.state, ev.typ)
		bn.postHandle(sess)
	}
}

func (bn *Net) loop(wg *sync.WaitGroup) {
	defer func() {
		wg.Done()
	}()

	for {
		select {
		case recvm := <-bn.recvQ:
			bn.recvHandle(recvm)
		case sendm := <-bn.sendQ:
			bn.sendHandle(sendm)
		case m := <-bn.eventQ:
			bn.eventHandle(m)
		case <-bn.stopC:
			alog.LogInfo(bn.lctx, "anet stopped")
			return
		}
	}
}
